package info.motteke.util;

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * {@link String}を扱うためのユーティリティクラスです。
 *
 * @author Y.Wakamatsu
 */
public final class StringUtil {

    private static final String DEFAULT_CHARSET = "UTF-8";

    /**
     * boolean型に変換する際、trueとして判定する文字列の配列です。
     */
    private static final String[] trueStrings = new String[] { "true", "yes", "ok", "on", "t", "y" };

    /**
     * 指定した文字セットを利用して、バイト配列から文字列を生成します。
     *
     * @param bytes
     *            バイト配列
     * @param charset
     *            文字セット
     * @return 新しい文字列
     */
    public static String create(byte[] bytes, String charset) {
        try {
            return new String(bytes, charset);
        } catch (UnsupportedEncodingException e) {
            throw new UnsupportedCharsetException(e);
        }
    }

    /**
     * 指定した文字セットを利用して、文字列をバイト配列に変換します。
     *
     * @param string
     *            文字列
     * @param charset
     *            文字セット
     * @return バイト配列
     */
    public static byte[] getBytes(String string, String charset) {
        try {
            return string.getBytes(charset);
        } catch (UnsupportedEncodingException e) {
            throw new UnsupportedCharsetException(e);
        }
    }

    /**
     * 文字列をbooleanに変換して返します。
     * 以下の文字列がtrueとなり、それ以外はfalseに変換します。
     * <ul>
     * <li>true</li>
     * <li>yes</li>
     * <li>ok</li>
     * <li>on</li>
     * <li>y</li>
     * <li>t</li>
     * <li>0以外の整数値</li>
     * </ul>
     *
     * @param string
     *            booleanに変換する文字列
     * @return stringが表すboolean
     */
    public static boolean parseBoolean(String string) {
        if (string == null) {
            return false;
        }
        for (String trueString : trueStrings) {
            if (trueString.equalsIgnoreCase(string)) {
                return true;
            }
        }

        return parseInt(string, 0) != 0;
    }

    /**
     * 文字列をbyteに変換して返します。<br>
     * 変換できない場合はdefを返します。
     *
     * @param string
     * @param def
     * @return
     */
    public static byte parseByte(String string, byte def) {
        if (string == null) {
            return def;
        }

        try {
            return Byte.parseByte(string);
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /**
     * 文字列をshortに変換して返します。<br>
     * 変換できない場合はdefを返します。
     *
     * @param string
     * @param def
     * @return
     */
    public static short parseShort(String string, short def) {
        if (string == null) {
            return def;
        }

        try {
            return Short.parseShort(string);
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /**
     * 文字列をint型に変換します。変換できない場合にはデフォルト値を返します。
     *
     * @param value
     *            文字列
     * @param def
     *            デフォルト値
     * @return 文字列が表す整数値
     */
    public static int parseInt(String value, int def) {
        if (value == null) {
            return def;
        }
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /**
     * 文字列をlong型に変換します。変換できない場合にはデフォルト値を返します。
     *
     * @param value
     *            文字列
     * @param def
     *            デフォルト値
     * @return 文字列が表す整数値
     */
    public static long parseLong(String string, long def) {
        if (string == null) {
            return def;
        }

        try {
            return Long.parseLong(string);
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /**
     * 文字列をfloat型に変換します。変換できない場合にはデフォルト値を返します。
     *
     * @param value
     *            文字列
     * @param def
     *            デフォルト値
     * @return 文字列が表す整数値
     */
    public static float parseFloat(String string, float def) {
        if (string == null) {
            return def;
        }

        try {
            return Float.parseFloat(string);
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /**
     * 文字列をdouble型に変換します。変換できない場合にはデフォルト値を返します。
     *
     * @param value
     *            文字列
     * @param def
     *            デフォルト値
     * @return 文字列が表す整数値
     */
    public static double parseDouble(String string, double def) {
        if (string == null) {
            return def;
        }

        try {
            return Double.parseDouble(string);
        } catch (NumberFormatException e) {
            return def;
        }
    }

    /**
     * 配列に格納されたオブジェクトを、その順で文字列化し、連結します。<br>
     * 連結する際、glueを間に挟みます。<br>
     *
     * objectsがnullのときは、空文字列を返します。<br>
     * glueがnullのときは、glueを空文字列として連結します。<br>
     *
     * @param glue
     *            連結する二つの文字列の間に挟む文字列
     * @param objects
     *            連結する文字列を格納した配列
     *
     * @return objectsが保持する要素を連結した文字列。間にglueが挟まれる。
     *
     */
    public static String implode(String glue, Object... objects) {

        if (objects == null) {
            return "";
        } else {
            return implode(glue, Arrays.asList(objects));
        }
    }

    /**
     * リストに格納された文字列を、その順で文字列化し、連結します。<br>
     * 連結する際、glueを間に挟みます。<br>
     *
     * objectsがnullのときは、空文字列を返します。<br>
     * glueがnullのときは、glueを空文字列として連結します。<br>
     *
     * @param glue
     *            連結する二つの文字列の間に挟む文字列
     * @param objects
     *            連結するオブジェクトを格納したリスト
     *
     * @return objectsが保持する要素を連結した文字列。間にglueが挟まれる。
     *
     */
    public static String implode(String glue, List<? extends Object> objects) {

        if (objects == null || objects.size() == 0) {
            return "";
        }

        if (glue == null) {
            glue = "";
        }

        StringBuilder buf = new StringBuilder();

        for (Object obj : objects) {
            buf.append(String.valueOf(obj)).append(glue);
        }
        int length = buf.length();
        buf.delete(length - glue.length(), length);

        return buf.toString();
    }

    /**
     * 文字列がnullあるいは空白文字列であるかを判断します。<br>
     *
     * @param string
     *            判断する文字列
     * @return stringがnullか空白文字列のときtrue、さもなくばfalse
     */
    public static boolean isBlank(String string) {
        return string == null || string.trim().length() == 0;
    }

    /**
     * 文字列がnullあるいは空文字であるかを判断します。
     *
     * @param string
     *            判断する文字列
     * @return stringがnullか空文字のときtrue、さもなくばfalse
     */
    public static boolean isEmpty(String string) {
        return string == null || string.length() == 0;
    }

    /**
     * バイト配列を16進数の文字列に変換します。<br>
     * 使用される文字は0から9までの数字とaからfまでの小文字のアルファベットです。
     *
     * @param bytes
     *            バイト配列
     * @return bytesを文字列化したもの
     */
    public static String toHexString(byte[] bytes) {
        StringBuilder buf = new StringBuilder(bytes.length * 2);

        for (byte b : bytes) {
            int n = b & 0xFF;

            // 一桁のときは0を補う
            if (n < 0x10) {
                buf.append('0');
            }
            // 16進数変換して加える
            buf.append(Integer.toHexString(n));
        }

        return buf.toString();
    }

    /**
     * デフォルトの文字セットを利用して、文字列のMD5ハッシュを作成します。
     *
     * @param str
     *            ハッシュを作成する文字列
     * @return md5ハッシュ
     */
    public static String md5(String str) {
        return md5(str, DEFAULT_CHARSET);
    }

    /**
     * 指定された文字セットを利用して、文字列のMD5ハッシュを作成します。
     *
     * @param str
     *            ハッシュを作成する文字列
     * @param charset
     *            文字セットを表す文字列
     * @return MD5ハッシュ
     */
    public static String md5(String str, String charset) {
        try {
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(getBytes(str, charset));
            return toHexString(md.digest());
        } catch (NoSuchAlgorithmException e) {
            throw new UnsupportedAlgorithmException("MD5はサポートされていません。");
        }
    }

    private static final Map<String, String> REF;

    static {
        Map<String, String> map = new HashMap<String, String>();
        map.put("nbsp", " ");
        map.put("gt", ">");
        map.put("lt", "<");
        map.put("amp", "&");

        REF = Collections.unmodifiableMap(map);
    }

    public static String htmlDecode(String string) {
        if (string == null) {
            return null;
        }
        int length = string.length();
        StringBuffer ret = new StringBuffer(length);

        for (int i = 0; i < length; i++) {
            char c = string.charAt(i);

            if (c != '&') {
                ret.append(c);
                continue;
            }

            StringBuffer buf = new StringBuffer();
            i++; // '&'をスキップする。
            while ((c = string.charAt(i)) != ';') {
                if (c == '&') {
                    System.out.println("セミコロンなし : " + buf);
                    ret.append(buf);
                    break;
                }

                buf.append(c);
                i++;
                if (i == length) {
                    System.out.println(buf);
                    ret.append(buf);
                    break;
                }
            }

            String ref = REF.get(buf.toString());
            if (ref != null) {
                System.out.println("参照発見 : " + buf + " -> " + ref);
                ret.append(ref);
            } else {
                String code = buf.substring(1);

                int n = parseInt(code, (int) '?');
                char ch = (char) n;
                System.out.println("文字参照 : " + code + "(" + n + ") -> " + ch);
                ret.append(ch);
            }
        }

        return ret.toString();
    }

    private static final Pattern P = Pattern.compile(
    "&#(x([0-9a-f]+)|([0-9]+));",
    Pattern.CASE_INSENSITIVE
    );

    private static boolean isHex(final String str) {
        final char x = str.charAt(0);
        return 'x' == x || 'X' == x;
    }

    public static String ncr(final String str) {
        final StringBuffer rtn = new StringBuffer();
        final Matcher matcher = P.matcher(str);
        while (matcher.find()) {
            final String group = matcher.group(1);
            int parseInt;
            if (isHex(group)) {
                parseInt = Integer.parseInt(group.substring(1), 16);
            } else {
                parseInt = Integer.parseInt(group, 10);
            }

            final char c;
            if (0 != (0x0ffff & parseInt)) {
                c = (char) parseInt;
            } else {
                c = '?';
            }
            matcher.appendReplacement(rtn, Character.toString(c));
        }
        matcher.appendTail(rtn);

        return rtn.toString();
    }

    public static void main(String[] args) {
        String name = "さなてち&#8252;";
        System.out.println(htmlDecode(name));
        System.out.println(ncr(name));
    }

    /**
     * コンストラクタです。
     * このクラスはインスタンス化出来ません。
     */
    private StringUtil() {
        throw new AssertionError();
    }
}
